#!/usr/bin/python2.5

import sys
import cgi
import urlparse
import SimpleHTTPServer
import BaseHTTPServer
import threading
import SocketServer
import socket
import os
import stat

# Import appscript from an egg, as that's how we're going to
# distribute it. Use __import__ to fool py2app into not crawling
# through the egg. In normal development, "import appscript" would be
# sufficient.
sys.path.append('appscript-0.21.1-py2.5-macosx-10.5-i386.egg')
appscript = __import__('appscript')

sys.path.append('protobuf-2.2.0-py2.5.egg')
from SynctasticProto_pb2 import *

# Import things appscript will need, since py2app won't check them for us.
import keyword
import pkg_resources

PORT = 28992
EXTENSIONS = ('mp3', 'm4a')

PAIRING_HTML = '''
<html>
<title>Synctastic Pony Pairing</title>
<body>
<form action="/pair">
Enter the pairing number you found on the Synctastic Pony app:
<input type="text" name="uuid">
<input type="submit" value="pair">
</form>
</body>
</html>
'''

def InstallAsLoginItem():
    """Install this program as a login item, if it's not already one."""
    APPNAME = "SynctasticPonyServer"
    PATH = '/Applications/SynctasticPonyServer.app'
    events = appscript.app('System Events')

    # If this is already a login item, exit
    for item in events.login_items():
        if item.name.get() == APPNAME:
            return

    # Add a new item.
    k = appscript.k
    events.login_items.end.make(new=k.login_item,
                                with_properties={k.path: PATH,
                                                 k.hidden: False})

class TrackList:
    def __init__(self):
        self.itunes = appscript.app('iTunes')
        self.paths = {}

    def PlaylistNames(self):
        """Return the name of all non-empty playlists"""
        playlists = []
        for p in self.itunes.user_playlists.get():
            if p.tracks.get():
                playlists.append(p.name.get())
        playlists.sort()
        return playlists

    def GetPlaylist(self, playlist_name):
        """Return a Playlist protocol buffer."""
        for p in self.itunes.user_playlists.get():
            name = p.name.get()
            if name != playlist_name: continue # TODO: Query for playlist
            playlist = Playlist()
            playlist.name = playlist_name
            for track in p.tracks.get():
                try:
                    path = track.location.get().path
                except appscript.reference.CommandError:
                    continue

                ext = path.split('.')[-1].lower()
                if ext not in EXTENSIONS:
                    continue

                t = playlist.track.add()
                t.remote_path = path
                t.size = os.stat(path)[stat.ST_SIZE]
                self.paths[path] = 1
            return playlist

    def PathSeenInPlaylist(self, path):
        return path in self.paths


class Server(SocketServer.TCPServer):
    allow_reuse_address = True
    def __init__(self, tracklist):
        SocketServer.TCPServer.__init__(self, ("", PORT), Handler)
        self.tracklist = tracklist
        self._ReadPairing()

    def _PairingFile(self):
        """Return the path to the file which stores the pairing information."""
        file = "%s/Library/Application Support/SynctasticPonyServer/pairing" % os.environ['HOME']
        return file

    def _ReadPairing(self):
        """Read the pairing uuid from a file."""
        try:
            self.paired_uuid = open(self._PairingFile()).readline().strip()
        except IOError:
            self.paired_uuid = None

    def PairWith(self, uuid, out):
        """Record the id of the phone we want to pair with."""
        self.paired_uuid = uuid
        f = self._PairingFile()
        if not os.path.exists(os.path.dirname(f)):
            os.makedirs(os.path.dirname(f))
        print >>open(f, 'w'), self.paired_uuid
        print >>out, 'Successfully paired'

    def GetPlaylists(self, handler):
        handler.end_headers()
        resp = PlaylistNames()
        for p in self.tracklist.PlaylistNames():
            resp.playlist.append(p)
        print resp
        handler.wfile.write(resp.SerializeToString())

    def GetPlaylist(self, handler, playlist):
        handler.end_headers()
        resp = self.tracklist.GetPlaylist(playlist)
        print resp
        handler.wfile.write(resp.SerializeToString())

    def GetFile(self, handler, path):
        if self.tracklist.PathSeenInPlaylist(path):
            handler.send_header('Content-Length', os.stat(path)[stat.ST_SIZE])
            handler.end_headers()
            handler.wfile.write(open(path).read())
        else:
            handler.end_headers()

class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
    def do_GET(self):
        p = urlparse.urlparse(self.path)
        path = p[2]
        query = cgi.parse_qs(p[4])
        print "PATH IS", p, path
        if not self.CheckPairing(query):
            self.send_response(403)
            self.end_headers()
            print >>self.wfile, 'Phone is not paired with this server'
            return
        if path not in self.HANDLERS:
            self.send_response(404)
        else:
            self.send_response(200)
            self.send_header('Content-Type', self.CONTENT_TYPES.get(path, 'text/plain'))
            self.HANDLERS[path](self, query)

    def GetPlaylists(self, query):
        self.server.GetPlaylists(self)

    def GetPlaylist(self, query):
        playlist = query['playlist'][0]
        self.server.GetPlaylist(self, playlist)

    def GetFile(self, query):
        file = query['file'][0]
        self.server.GetFile(self, file)

    def GetFavico(self, query):
        pass  # dummy implementation to suppress errors

    def Pair(self, query):
        """Record the pairing uuid or send the form to enter it. If the visitor isn't from localhost, reject."""
        self.end_headers()
        if self.client_address[0] != '127.0.0.1':
            print >>self.out, 'Pairing is only accessible from the computer you want to pair with.'
            return
        if 'uuid' in query:
            self.server.PairWith(query['uuid'][0], self.wfile)
        else:
            print >>self.wfile, PAIRING_HTML

    def CheckPairing(self, query):
        """Return whether the query's uuid parameter matches the id we've paired against."""
        # Anyone is allowed to visit /pair
        if self.path.startswith('/pair'):
            return True
        if 'uuid' not in query:
            return False
        try:
            uuid = query['uuid'][0]
        except:
            return False
        return uuid == self.server.paired_uuid

    HANDLERS = {
        '/playlists' : GetPlaylists,
        '/playlist' : GetPlaylist,
        '/file' : GetFile,
        '/favicon.ico' : GetFavico,
        '/pair' : Pair,
        }

    # Default content type is text/plain
    CONTENT_TYPES = {
        '/pair' : 'text/html'
        }

class AnnounceThread(threading.Thread):
    REQUEST_KEY = "I LOVE PONIES"
    RESPONSE_KEY = "PONIES ARE AWESOME"
    def __init__(self):
        threading.Thread.__init__(self)
        self.setDaemon(True)
        self.start()

    def run(self):
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.bind(('0.0.0.0', PORT))
        while 1:
            msg, addr = s.recvfrom(65536)
            print "RECEIVED", addr, msg
            if not msg.startswith(self.REQUEST_KEY):
                continue
            req = AnnounceRequest()
            try:
                req.ParseFromString(msg[len(self.REQUEST_KEY):])
            except Exception, e:
                print e
                continue
            print req

            send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            resp = AnnounceResponse()
            resp.hostname = socket.gethostname()
            resp.port = PORT
            resp.address ='blah2'
            response = self.RESPONSE_KEY + resp.SerializeToString()
            print "SENDING", resp
            send.sendto(response, addr)

InstallAsLoginItem()
t = TrackList()
httpd = Server(t)
announce = AnnounceThread()
try:
    httpd.serve_forever()
except KeyboardInterrupt:
    pass
finally:
    httpd.server_close()
